cat 命令的源码进化史
(给Linux爱好者加星标,提升Linux技能)
编译:伯乐在线/一汀
有一次,我跟我的亲戚有一场争论,是关于读一个计算机科学的学位是否值得。当时是我在大学里面临是否选择计算机科学专业的时候。我姑姑和一个表哥认为我不该选。他们觉得会编程当然是个既有用又合算的事情,但是他们也坚信,计算机科学更新太快了,当下学到的知识会很快被淘汰掉。所以最好是选一门编程的课程,然后主修经济或者物理这种基本知识一辈子都适用的专业。
我并不相信他们的理论,并且选择了主修计算机专业(抱歉了姑姑和表哥!)其实不难看出,为什么常人会认为计算机科学,或者软件工程这样的专业,每几年就会更新换代。先是诞生了私人计算机,然后是网络,手机,机器学习……科技永远在变化,那么其潜在的技术原理当然也在变化了。当然,最让人惊讶的是,这些基础技术原理,其实基本没变。我相信大部分人要是知道他们计算机中重要软件到底有多老,肯定会震惊。我并不是说软件的表面,毕竟我自己用的最多的火狐浏览器,两周前才更新过。但是如果你打开帮助手册查看 grep 之类的工具,你会发现它的上一次更新还是在 2010 年(至少 Mac 系统是这样)。grep 的初代诞生于 1974 年,那时候的计算机时代好比侏罗纪。现如今,人们(以及程序)在工作中仍然要依赖 grep 做很多事情。
我姑姑和表哥把计算机科技想象成一系列沙滩上的城堡,涨潮时潮水抹去旧的城堡,更加华丽的新城堡又会被建成。其实在现实中的很多领域,我们都是不断地在现有的程序基础上进行迭代。我们也许会时不时的修改这些程序来避免软件崩溃,但是除此之外这些程序不需要额外的维护。grep 是一个简单的程序,它所解决的问题现在也有意义,所以它至今还存在。很多应用程序的编写都起始于一个很高的角度,就像是在金字塔顶端的基础上构建,而金字塔本身是由曾经解决问题的答案所建成的。现在看来很陈旧的,三四十年前的想法与概念,在很多时候都融入到了你现在计算机上安装了的应用程序里。
我想仔细研究一个这样的老程序,看看它从诞生到现在到底被修改了多少次,这肯定很有趣。我想用 cat 这个最简单的 Unix 工具来作为例子。Ken Thompson 在 1969 年开发了初代 cat。如果我跟别人说我计算机里有个 1969 年的程序,这准确吗?cat 在这几十年里到底经历了几次迭代?我们计算机里的程序到底有多古老?
幸好有这个代码仓库,我们可以清晰地了解到,从 1969 年以来,cat 是如何进化的。我接下来会主要聚焦于我自己 Macbook 上 cat 程序的历史实现方式。你会看到,cat 历史从最初的 Unix 版本,到现在的 Mac 版本,这个程序被重写了比你预想的还要多的次数,但是最终它所实现的功能几乎跟五十年前一模一样。
Unix实验版本
1969 年,Ken Thompson 和 Dennis Ritchie 开始在 PDP 7 上开发 Unix。这是在 C 语言出现之前,所以早期的 Unix 程序都是用 PDP 7 上用汇编语言开发的。他们使用了专门针对于 Unix 的汇编版本,因为 Ken Thompson 开发了自己的汇编编译器,他在 PDP 7 出厂商DEC 提供的编译器基础上添加了新的功能。Thompson 的改进文档在初始 Unix 编程手册中有收录,在 as 编译器条目下面。
cat 的初代实现使用了 PDP 7 汇编语言。我有添加一些注释来解释每行命令,但是除非你明白 Thompson 编写汇编编译器的一些扩展,不然这个程序还是很难理解。这里有两个重要的点。第一,字符 ; 可以被用于分隔同一行的声明语句。根据 sys 指令的描述, ; 通常被用于在同一行使用系统调用参数。第二,Thompson 添加了数字 0-9 用于支持“暂存标记”。这些标记可以被整个程序重用,这就像 Unix 编程手册所描述的,“对于程序员思维和汇编语言字符空间的缩减优化”。从手册中,你可以使用 nf 来表示下一个标记 n,用 nb 来表示上一个标记 n。举个例子,如果你有个标记为 1: 的代码块,你可以从相距很远的下方代码中使用 jmp 1b 来往上跳回标记代码。(但是你不能往下跳到标记代码,除非你使用jmp 1f。)
关于初代 cat 最有意思的是,它包含了两个我们熟知的名字,分别是一个标记为是一个标记为 getc,和一个标记为 putc 的代码块,这表示这俩名字要比标准 C 语言库都要历史久远。初代 cat 实际上包含了这两个方法的实现。这样的实现方式使得输入字符可以被写入缓冲区,也就是说,读和写不需要以单个字符为单位完成。
初代 cat 并没有存在很久。Ken Thompson 和 Dennis Ritchie 成功劝说了贝尔实验室帮他们购入了一台 PDP11,以便于他们对 Unix 系统进行扩展与提高。PDP 11 使用的是一种不同的指令集,因此他们不得不重写 cat。对于 第二代 cat 代码我也加了注释。第二代使用了针对于新指令集的新版汇编助记符,也利用了 PDP 11中不同的地址模式。(那些源代码中的括号和 $ 符号,是被用来指代不同的地址模式的。)但是 cat 第二代中也同样使用了初代中的 ; 和暂存标记,这些功能一定是在 PDP 11 中移植 as 时被保留了下来。
cat 的第二代源代码远比初代要简洁很多。第二代也更加的”Unix-y”,因为它不再需要一串文件名作为命令参数,而是与如今的 cat一样,在没有参数的情况下,从 stdin 读取输入。对于二代 cat,你也可以使用参数来指定从 stdin 读取输入数据。
1973 年,为了准备发布第四版 Unix,很大一部分 Unix 系统都用 C 语言重写了一遍。但是 C 语言版本的 cat 在 Unix 发布后过了一段时间才出现。第一个 C 语言版本的 cat 只出现在第七版 Unix 系统中。这个实现方法非常值得一读,因为它非常简单明了。与其他版本比较,这一版最能作为代表 cat 的 K&R C 语言教育演示版本。这段程序的核心就是如下两行:
while ((c = getc(fi)) != EOF)
putchar(c);
当然还有更多的代码,但是除了这两行以外,剩下的逻辑更多的是在确保用户不会同时读写同一个文件。另一个有意思的地方是,这个版本的 cat 只认得一个标记,-u。这个 -u 标记可以被用于关闭输入输出缓冲区,不然 cat 会默认缓存 512 字节。
伯克利软件套件/BSD
在第七版之后,Unix 催生了各种各样的衍生品。MacOS 是基于 Darwin 系统的,而 Darwin 是基于伯克利软件套件(BSD),因此 BSD 是我们最感兴趣的 Unix 分支。BSD 最初是作为Unix附加功能的软件合集,但是它最终成为了一个完整的操作系统。BSD似乎一直在用cat的初代版本,一直到第四版 BSD 发布为止。第四版 BSD 也就是 4BSD,它添加了对于新标记的支持。4BSD 版本的 cat 能明显的看出是初代的衍生品,不过它添加了一些新的函数用来实现用新标记触发的功能。4BSD 文件系统的命名方法是基于 fflg 这个变量的,fflg 用于标记指令的输入是从文件,还是 stdin 读取的。继 fflg 之后,nflg、bflg、vflg、sflg、eflg 和 tflg 也被用于记录程序中的标记是否被用到。这些命令行标记是 cat 添加的最后一批标记;如今至少在 Mac 系统中的 cat 命令行手册有列出来这些标记。4BSD 是在 1980 年发布的,所以这一系列的标记有 38 岁了。
cat 最后一次被重写是为了 BSD Net/2,这主要是为了避免软件许可证问题,因此所有 AT&T Unix 衍生代码都被替换为了新代码。BSD Net/2 在 1991 年发布。最后一次重写是由 Kevin Fall 完成的,Kevin Fall 于 1988 年毕业于伯克利,之后他花了一年的时间在计算机系统研究院(CSRG)工作了一年。Fall 告诉我,用 AT&T 代码写的 Unix 工具集列表被挂在了 CSRG 的一面墙上,员工们被告知可以选择感兴趣的工具重写。Fall 选择了 cat 和 mknod。在如今 Mac 系统的默认 cat 版本中,Fall 的名字排在开发者名单前列。他所编写的 cat,虽然是个很简单的程序,但是直到今年还有数百万的用户在使用。
Fall 所写的 cat 源代码比我们之前看到的版本要长许多。除了支持 -? 帮助标记,这一版并没有添加新的功能。理论上来说,这一版代码与 4BSD 版本非常相似。代码之所以长,是因为 Fall 分开了“旧版”和“新版”的逻辑。“旧版”是典型的 cat;它一个字符一个字符的输出。“新版”的 cat 包括了 4BSD 命令行选项。这样的分割很有道理,但是使得代码在第一眼看上去比实际复杂很多。代码的最后有个华丽的错误处理方程,这也增加了代码长度。
MacOS
2001 年,苹果公司发布了 Mac OS X 系统。这次发布对于苹果公司来说非常重要,因为他们花了很多年,走了不少弯路,为了研发能够取代存在了很多年的旧版 Mac OS 系统。苹果公司内部曾经有过两次研发新系统的尝试,但是最终都没能成功;后来,苹果收购了史蒂夫·乔布斯的公司 NeXT,他们公司开发了一款名为 NeXTSTEP 的,基于面向对象编程框架的操作系统。苹果决定使用 NeXTSTEP 作为Mac OS X 的基础。NeXTSTEP 的一部分是基于 BSD 开发的,所以用 NeXTSTEP 作为 Mac OS X 的基础,同时也给苹果系统带来了 BSD 代码风格。
新发布的第一版 Mac OS X中包含了来自 NetBSD 项目的 cat 代码实现。NetBSD 项目如今仍在不断开发中,它最初是来自 386BSD 的分支。而 386BSD 是直接基于 BSD Net/2 的。所以 Mac OS X 上的 cat 就是 Kevin Fall 所写的 cat。唯一变化的是,Kevin Fall 写的错误处理函数 err() 被替换成了 err.h 中的 err()。err.h 是 BSD 基于 C 语言标准库的扩展。
NetBSD 版本的 cat 在不久之后被 FreeBSD 版本取代了。根据维基百科,苹果从 Mac OS X 10.3 (Panther)开始,使用 FreeBSD 来取代 NetBSD。但是 Mac OS X 版本的 cat,根据苹果的开软发布记录,一直到 2007 年发布 Mac OS X 10.5 (Leopard) 才被取代。苹果为了发布 Leopard 而引进的 FreeBSD 的实现版本一直被沿用到了今天。从 2007 一直到 2018 年,这一版没有做过任何升级或者改变。
所以说 Mac OS 中的 cat 是古老的。实际上 cat 的出现,比 2007 年的正式发布时间还早两年。2005 年的改动,在 FreeBSD 的Github 镜像中可以看到,是 cat 被移植到 Mac OS X 之前 FreeBSD 版的最后一次更新。所以 Mac OS X 中 cat 实际上有 13 年的历史了,它并没有与 FreeBSD 的 cat 进行同步更新。这里有过一个辩论,软件到底被改动过几次才算是一个新的软件呢;就 cat 这个个例来看,它的源代码从 2005 年开始就完全没有改变过了。
如今 Mac OS 系统中的 cat 与 Fall 在 1991 年为 BSD Net/2 所写的版本并没有太多不同。最大的不同是添加了一个新的函数用来支持 Unix 上的套接字。一个 FreeBSD 的开发者认为 Fall 所写的 raw_args() 函数应该与 cook_args() 合并为一个函数 scanfiles()。除此之外,最核心的部分还是 Fall 的代码。
我问过 Fall,有几百万苹果用户在使用你所写的 cat,还有很多程序直接或者间接依赖 cat,对此你有什么感想。如今已经是顾问兼最新版 TCP/IP 协议合作者的 Fall 表示,人们对他开发 cat 的经历如此的感兴趣,让他觉得非常惊讶。Fall 曾经在计算领域工作过很久,并且有过很多有影响力的项目经历。但是似乎人们对于他在 1989 年开发 cat 的那六个月更加感兴趣。
百岁程序
纵观历史上各种伟大的发明,计算机的历史并没有很久。我们仍然在使用有着百年历史的照片和胶卷。但是计算机软件是另外一个类别——目前仍属于高新科技。至少现在的软件是这样。随着计算机产业日渐成熟,我们会不会有一天发现,我们在使用有着百年历史的软件呢?
计算机硬件最终也会更新换代,现在的软件想必是没法跑在一个世纪以后的硬件上。也许高级语言设计的进步,也会导致在将来没有人会使用 C 语言,而 cat 也会被其他的语言重写。(不过 C 语言已经存在了五十年了,估计短期内也不会被取代。)不考虑以上这些的话,不如我们就一直用现在这版 cat 吧。
我认为,cat 的历史告诉我们,在计算机科学领域有一些思想是非常耐用的。实际上,对于 cat,它的代码和思想都是很多年前出现的。要说我计算机中的cat是1969年的其实并不准确。但如果说我计算机中的 cat 是 1989 年 Fall 开发的,就准确多了。很多软件都很古老。也许我们不能单纯的认为计算机科学和软件开发是不断更新换代的领域。我们所开发的系统都是基于历史基础的。在某些时候,我们在开发新代码的同时,也需要去花时间去理解和维护历史代码。
推荐阅读
(点击标题可跳转阅读)
看完本文有收获?请分享给更多人
关注「Linux 爱好者」加星标,提升Linux技能
喜欢就点一下「好看」呗~